-
Notifications
You must be signed in to change notification settings - Fork 277
[TG-9002] Symex: revert guards after call return #5003
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[TG-9002] Symex: revert guards after call return #5003
Conversation
Poster-child example courtesy of @hannes-steffenhagen-diffblue: #include <assert.h>
#include <string.h>
#include <stdlib.h>
char *nondet_str(void) {
const char empty[] = " ";
char *result = strdup(empty);
int x;
__CPROVER_assume(x >= 0 && x <= 9);
result[30] = (char)('0'+x);
return result;
}
int main(void) {
long x = 2;
const char *sx = " 2";
for(int i = 0; i < 10; ++i) {
assert(strtol(nondet_str(), NULL, 10) == x);
}
return 0;
} This previously got bogged down in an ever-growing guard, I think because the guard |
e7525ef
to
a1a7b6c
Compare
Guards getting fat & monstrous has bugged me for many years -- and I believe it's a major performance problem. |
Another BTW: I think that path-pased symex suffers from this just as much. |
@kroening I don't follow "function body"? |
SVCOMP/Java: 3 tests stop timing out (timeout = 30 seconds), overall CPU time down 5% |
SVCOMP/C subsample: 3 tests stop timing out (timeout = 30 seconds), total CPU time down 2% |
a1a7b6c
to
9c9c883
Compare
CI failure: this harms performance for cases with many conditional Note this already happens now when paths are truncated by falsifying the guard -- for example exceeding Proposed course of action: merge this PR with the guard reversion but not the path truncation cleanup, then separately pursue guard shrinkage when paths are truncated, then make path truncation uniform so that we don't execute instructions after an |
In view of this I've removed the cleanup of |
0911519
to
ccf6391
Compare
Current status: causes about 10 SVCOMP/Java tests to stop timing out and instead return incorrect answers. Looking into it... |
Codecov Report
@@ Coverage Diff @@
## develop #5003 +/- ##
===========================================
- Coverage 69.47% 69.43% -0.05%
===========================================
Files 1310 1310
Lines 108763 108741 -22
===========================================
- Hits 75566 75502 -64
- Misses 33197 33239 +42
Continue to review full report at Codecov.
|
Conclusion: reintroduced those parts of the cleanup that ensure an ASSUME / real constraint is generated wherever we truncate execution due to the unwind or depth limits, as otherwise using the dominant guard would actually mean broadening the guard by re-including impossible paths. This PR now (a) ensures we generate a real constraint whenever we truncate a path, and (b) uses guard-as-of-entering-function whenever we exit that function, possibly broadening it by including paths that have been ruled out and/or simplifying it better than the expression-simplifier is capable of. Forthcoming work will separate the concepts of guards and reachability, such that we can reduce the execution guard at control-flow convergances even when one of the predecessors is unreachable, while maintaining the benefit of not generating constraints for unreachable code. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✔️
Passed Diffblue compatibility checks (cbmc commit: 9aec581).
Build URL: https://travis-ci.com/diffblue/test-gen/builds/122999685
Re-benchmarked with neutral results: all I claim now is that specific examples where the guard gets out of hand, like Hannes' example above, become analysable with this change but were previously impractically slow. |
main::1::i!0@1#[0-9]+ = 2 | ||
-- | ||
This checks that code rendered unreachable by a __CPROVER_assume(0) does not generate constraints. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
redundant blank line
Executing a function may have a cumulative effect on the state guard. For example, if the callee contained ASSUME statements that rendered one or more control-flow options unviable then the guard might still embody that restriction (i.e. for if(x) ASSUME(false) the guard might still be `!x`). However, on function return we know that all control-flow paths have converged or been shown to be unviable, so we can restore the simpler guard as it was when we entered the callee function. Exceptions: (a) if the guard is false it would be correct but inefficient to restore it; keep it false until we find a convergeance with another viable path (b) if we're doing path-sensitive symex then we do tail duplication, and there are no control-flow convergeances. Keep the guard as-was. (c) if we're executing a multi-threaded program then symex_assume_l2 uses the guard to accumulate assumptions, which we must not discard. In truth this optimisation is applicable whenever a block postdominates another, but function structure gives us a cheap way to establish postdominance without analysis (in the absence of setjmp/longjmp at least)
Whenever we truncate a particular execution path, whether due to --unwind or --depth settings, we should generate an assumption so that it is not necessary to record the rejected path in the state guard. For example, before if we exceeded the depth limit then we would set the guard to false, and rely on carrying this missing path forwards to any future assume or assert statement. By emitting an assume we explicitly make a constraint ruling this path out, meaning that downstream guards no longer need to carry that information (for example, where before we might have had guard `x` and VCCs `!x => y` and `!x => z`, we can now emit a constraint `!x`, restore the guard to `true` and produce VCCs `y` and `z`). Note this commit does not actually change when the guard is set false and when it is not, as this will have additional performance consequences and should be benchmarked independently.
91636b4
to
66133a6
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR failed Diffblue compatibility checks (cbmc commit: 5dbbf29).
Build URL: https://travis-ci.com/diffblue/test-gen/builds/123209903
Status will be re-evaluated on next push.
Common spurious failures include: the cbmc commit has disappeared in the mean time (e.g. in a force-push); the author is not in the list of contributors (e.g. first-time contributors); compatibility was already broken by an earlier merge.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✔️
Passed Diffblue compatibility checks (cbmc commit: 66133a6).
Build URL: https://travis-ci.com/diffblue/test-gen/builds/123215484
Executing a function may have a cumulative effect on the state guard. For example, if the callee contained ASSUME statements that rendered one or more control-flow options unviable then the guard might still embody that restriction (i.e. for if(x) ASSUME(false) the guard might still be
!x
). However, on function return we know that all control-flow paths have converged or been shown to be unviable, so we can restore the simpler guard as it was when we entered the callee function.Exceptions:
(a) if the guard is false it would be correct but inefficient to restore it; keep it false until we find a convergeance with another viable path
(b) if we're doing path-sensitive symex then we do tail duplication, and there are no control-flow convergeances. Keep the guard as-was.
(c) if we're executing a multi-threaded program then symex_assume_l2 uses the guard to accumulate assumptions, which we must not discard.
In truth this optimisation is applicable whenever a block postdominates another, but function structure gives us a cheap way to establish postdominance without analysis (in the absence of setjmp/longjmp at least)